import {
  CSDashLine,
  CSLine,
  CSPoint,
  EPSILON,
  EventsRes,
  IArcOfCircleParams,
  ICircleParams,
  ICurve,
  IEllipseParams,
  ILineParams,
  MouseInfo,
  POINT_TYPE,
  SketchFrame,
  Tuple3,
} from '@amcax/base';
import { SKETCH_EVENT } from '@app-base/common/Constants';
import { sketchEventCenter } from '@app-base/environments/nextcad/events/SketchEventCenter';
import { ctx2, userEvents } from '@app-base/module';
import { isEqual } from 'lodash';
import { Object3D, Shape, Vector2, Vector3, Vector3Tuple } from 'three-legacy';
import { Vector2Tuple } from 'three/src/Three.Core.js';
import { sketchStore } from './useSketchStore';
import { Picker2D, PickerMgr } from '@amcax/renderer';

// 点的吸附类型
export enum SnapType {
  Point, // 点重合
  Line, // 点在直线上
  Circle, // 点在圆上
  Ellipse, // 点在椭圆上
  Spline, // 点在样条线上
}

export type PointSnapParam = {
  point: Vector2Tuple;
};

export type LineSnapParam = {
  point: Vector2Tuple;
  direction: Vector2Tuple;
};

export type CircleSnapParam = {
  center: Vector2Tuple;
  radius: number;
};

export type EllipseSnapParam = {
  center: Vector2Tuple;
  majorDirection: Vector2Tuple;
  major: number;
  minor: number;
};

interface SnapTypeMap {
  [SnapType.Point]: PointSnapParam;
  [SnapType.Line]: LineSnapParam;
  [SnapType.Circle]: CircleSnapParam;
  [SnapType.Ellipse]: EllipseSnapParam;
  [SnapType.Spline]: undefined;
}

export type SnapInfo<T extends SnapType = SnapType> = {
  type: T;
  // 如果点是吸附在已有的线上，则为吸附的曲线，不然则为空
  curve?: ICurve;
  // 如果是临时点，需要生成id
  tempPointId?: string;
  tempPoint?: Vector2Tuple;
  param: SnapTypeMap[T];
  isExtend?: boolean;
};

export type SnapResult = {
  point: Vector2Tuple;
  info: SnapInfo[];
};

export class SnapManager {
  frame: Tuple3<Vector3Tuple>;
  public sketchFrame: SketchFrame;
  previewPoint: CSPoint;
  extendLinePreviews: Map<string, CSDashLine> = new Map();

  currentSnapConstaints: SnapInfo[];

  updateFrame() {
    const { frame } = sketchEventCenter().getSketchInfo();

    this.frame = frame as Tuple3<Vector3Tuple>;
    this.sketchFrame = new SketchFrame(frame);
  }
  constructor() {
    this.updateFrame();
    userEvents.on(SKETCH_EVENT.SKETCH_EVENT_CHANGE, ({ type }) => {
      if (type === 'sketchIn') {
        this.updateFrame();
      }

      Array.from(this.extendLinePreviews.values()).forEach((v) => {
        v.parent?.remove(v);
        v.destroy();
      });

      this.extendLinePreviews.clear();
    });
  }

  // 点吸附
  // 优先级为：
  // 1、拾取点
  // 2、临时点的水平/垂直
  // 3、拾取线
  // 4、直线/圆弧的延长线
  snap(
    e: EventsRes<PointerEvent>,
    settings: {
      needPick?: boolean;
      needExtend?: boolean;
      needDraw?: boolean;
    } = {
      needPick: true,
      needExtend: true,
      needDraw: true,
    },
  ) {
    const mousePoint = this.getPointByMouse(e.mouse);

    const snapResult: SnapInfo[] = [];

    const tempPointsSnapResult = settings.needExtend
      ? this.snapTempPoints(mousePoint)
      : [];

    tempPointsSnapResult.forEach((s) => {
      this.pushSnapIfDifferentAndLessThan2(snapResult, s);
    });

    const pickResult = settings.needPick ? this.pickObjects(e) : [];

    pickResult.forEach((s) => {
      this.pushSnapIfDifferentAndLessThan2(snapResult, s);
    });

    const otherSnap = settings.needExtend
      ? this.snapExtendLineOrArc(
          mousePoint,
          snapResult.map((r) => r.curve?.id).filter(Boolean),
        )
      : [];

    otherSnap.forEach((s) => {
      this.pushSnapIfDifferentAndLessThan2(snapResult, s);
    });

    const mousePointArray: Vector2Tuple = [mousePoint.x, mousePoint.y];

    let result: SnapResult;
    if (snapResult.length === 0) {
      result = null;
    } else {
      const cross =
        snapResult.length <= 1
          ? null
          : this.calcCross(snapResult[0], snapResult[1], mousePointArray);

      const distanceToMouse =
        cross == null
          ? 0
          : new Vector2(
              cross[0] - mousePoint.x,
              cross[1] - mousePoint.y,
            ).length();

      const epsilon = 12 / this.cameraZoom;

      if (cross == null || distanceToMouse > epsilon) {
        result = {
          point: this.calcNearestPoint(mousePointArray, snapResult[0]),
          info: [snapResult[0]],
        };
      } else {
        result = {
          point: cross,
          info: snapResult,
        };
      }
    }

    if (!settings.needDraw) {
      Array.from(this.extendLinePreviews.values()).forEach((v) => {
        v.parent?.remove(v);
        v.destroy();
      });

      this.previewPoint?.destroy();
      this.previewPoint?.parent?.remove(this.previewPoint);

      this.extendLinePreviews.clear();
    } else {
      if (this.previewPoint != null) {
        this.previewPoint.destroy();
        this.previewPoint.parent?.remove(this.previewPoint);
      }

      if (result != null) {
        this.previewPoint = new CSPoint({
          vertex: this.sketchFrame.toWorld(new Vector3(...result.point)),
          pointType: POINT_TYPE.MIDPOINT,
        });
        this.previewPoint.hoverOn();
        this.previewPoint.setSize(8);
        ctx2.cadScene.assisGroup.add(this.previewPoint);
      }

      const newLines = new Map<string, CSDashLine>();
      result?.info?.forEach((snapInfo) => {
        if (snapInfo.isExtend) {
          const lineId = snapInfo.curve?.id ?? snapInfo.tempPointId;

          const currentLine = this.extendLinePreviews.get(lineId);

          let vertexs: Vector3[];

          if (snapInfo.type === SnapType.Line) {
            const endPoint = result.point;
            if (snapInfo.curve != null) {
              const distances = snapInfo.curve.vertices.map((v) => {
                return new Vector2(
                  result.point[0] - v[0],
                  result.point[1] - v[1],
                ).length();
              });

              vertexs = [
                new Vector3(
                  ...(distances[0] > distances[1]
                    ? snapInfo.curve.vertices[1]
                    : snapInfo.curve.vertices[0]),
                ),
                new Vector3(...endPoint),
              ].map((v) => this.sketchFrame.toWorld(v));
            } else {
              vertexs = [
                (snapInfo.param as LineSnapParam).point,
                snapInfo.tempPoint,
              ].map((v) => this.sketchFrame.toWorld(new Vector3(...v)));
            }
          }
          if (snapInfo.type === SnapType.Circle) {
            const param = snapInfo.curve.params as IArcOfCircleParams;
            const distances = [param.point1, param.point2].map((v) => {
              return new Vector2(
                result.point[0] - v[0],
                result.point[1] - v[1],
              ).length();
            });

            const isStart = distances[0] < distances[1];
            const isClockwise = isStart;
            const startPoint = isStart ? param.point1 : param.point2;

            const getAngle = (v: Vector2Tuple) => {
              const vec = new Vector2(
                v[0] - param.center[0],
                v[1] - param.center[1],
              );

              return vec.angle();
            };

            const shape = new Shape().absarc(
              param.center[0],
              param.center[1],
              param.radius,
              getAngle(startPoint),
              getAngle(result.point),
              isClockwise,
            );
            vertexs = shape
              .getPoints(25)
              .map((v) => this.sketchFrame.toWorld(new Vector3(v.x, v.y)));
          }

          const removeCurrentLine = () => {
            currentLine?.parent?.remove(currentLine);
            currentLine?.destroy();
          };

          if (vertexs == null) {
            removeCurrentLine();
          } else if (
            currentLine != null &&
            currentLine.getVertexs()?.length === vertexs.length &&
            currentLine.parent != null
          ) {
            currentLine.update({ vertexs });
            newLines.set(lineId, currentLine);
          } else {
            removeCurrentLine();
            const newLine = new CSDashLine({
              vertexs,
              dashed: true,
              lineWidth: 1,
              dashSize: 5,
              gapSize: 3,
              depthTest: false,
              color: 0xf7a023,
            });
            newLines.set(lineId, newLine);

            ctx2.cadScene.assisGroup.add(newLine);
          }
        }
      });

      const lineArray = Array.from(newLines.values());
      Array.from(this.extendLinePreviews.values()).forEach((v) => {
        if (!lineArray.includes(v)) {
          v.parent?.remove(v);
          v.destroy();
        }
      });

      this.extendLinePreviews.clear();

      Array.from(newLines.keys()).forEach((id) => {
        this.extendLinePreviews.set(id, newLines.get(id));
      });
    }

    ctx2.requestRender();

    return result;
  }

  tempSnapPoints: Vector2Tuple[] = [];
  // 命令中使用的临时点
  // 可以吸附到点的水平/竖直位置上
  addTempSnapPoints(point: Vector3) {
    this.tempSnapPoints.push([point.x, point.y]);
  }

  clearTempSnapPoint() {
    this.tempSnapPoints = [];
  }

  // 吸附到目标后的处理
  // 若吸附结果为点，则最终结果应当只有一个点，此时点就已经没有自由度
  // 若吸附结果为线
  // 仅当吸附结果与当前已有结果不为同约束时，加入结果列表
  // 结果列表有两个元素时，说明点已不再有自由度，故不再改变
  pushSnapIfDifferentAndLessThan2(snaps: SnapInfo[], newSnap: SnapInfo) {
    if (newSnap.type === SnapType.Point) {
      snaps.splice(0, 2);
      snaps.push(newSnap);
    } else if (
      snaps.length < 2 &&
      snaps.every(
        (s) => !this.isSameConstraint(s, newSnap) && s.type !== SnapType.Point,
      )
    ) {
      snaps.push(newSnap);
    }
  }

  snapTempPoints(mousePoint: Vector3) {
    const epsilon = 6 / this.cameraZoom;
    const result: SnapInfo[] = [];

    this.tempSnapPoints.forEach((p) => {
      if (result.length >= 2) {
        return;
      }
      if (Math.abs(p[0] - mousePoint.x) < epsilon) {
        this.pushSnapIfDifferentAndLessThan2(result, {
          type: SnapType.Line,
          param: {
            point: [p[0], mousePoint.y],
            direction: [0, 1],
          },
          isExtend: true,
          tempPointId: Date.now().toString() + Math.random().toFixed(4),
          tempPoint: p,
        });
      } else if (Math.abs(p[1] - mousePoint.y) < epsilon) {
        this.pushSnapIfDifferentAndLessThan2(result, {
          type: SnapType.Line,
          param: {
            point: [mousePoint.x, p[1]],
            direction: [1, 0],
          },
          isExtend: true,
          tempPointId: Date.now().toString() + Math.random().toFixed(4),
          tempPoint: p,
        });
      }
    });

    return result;
  }

  // 不使用picker2d，因为此处需要所有的raycast结果，picker2d不支持该行为
  pickObjects(e: EventsRes<PointerEvent>) {
    const raycaster = ctx2.viewer.sceneSetup.createRaycaster(
      e.mouse.x,
      e.mouse.y,
      0,
      10, // 草图的线可能距离比较近，所以设置一个较小的threshold
    );
    const intersections = raycaster
      .intersectObjects(
        ctx2.cadScene.sketchGroup.children.filter(
          (c) =>
            !(
              c instanceof CSPoint &&
              (c.userData?.info as ICurve)?.id?.includes('AXIS')
            ),
        ),
        true,
      )
      .filter((item) => item.object.userData?.info?.id != null); // 过滤掉没有info的对象 (草图中的物体对像必有info对象)

    let pickedPoint: ICurve;
    const otherPickedObjects: ICurve[] = [];
    const pickedObjects: Object3D[] = [];

    intersections.forEach((intersection) => {
      if (pickedPoint != null) {
        return;
      }
      const info = intersection.object.userData?.info as ICurve;
      if (info == null) {
        return;
      }

      if (info.type === 'Point') {
        pickedObjects.push(intersection.object);
        pickedPoint = info;
        return;
      }

      pickedObjects.push(intersection.object);
      otherPickedObjects.push(info);
    });

    PickerMgr.getPicker<Picker2D>('2d').updateHoverObjects(
      pickedObjects as (CSLine | CSPoint)[],
    );

    if (pickedPoint) {
      return [this.parseICurveToSnapInfo(pickedPoint)];
    }

    const leadingDifferentSnaps: SnapInfo[] = [];

    otherPickedObjects.forEach((i) => {
      if (leadingDifferentSnaps.length === 2) {
        return;
      }

      if (leadingDifferentSnaps.length === 0) {
        leadingDifferentSnaps.push(this.parseICurveToSnapInfo(i));
        return;
      }

      const newSnap = this.parseICurveToSnapInfo(i);

      this.pushSnapIfDifferentAndLessThan2(leadingDifferentSnaps, newSnap);
    });

    return leadingDifferentSnaps;
  }

  get cameraZoom() {
    return ctx2.getCameraZoom();
  }

  // 获取直线/圆弧延长线
  snapExtendLineOrArc(mousePoint: Vector3, excludeIds: string[]) {
    const epsilon = 6 / this.cameraZoom;

    const allCurves = sketchStore.getState().curves;

    const snaps: SnapInfo[] = [];

    allCurves.forEach((c) => {
      if (excludeIds.includes(c.id) || snaps.length >= 2) {
        return;
      }
      // if (c.type === 'Point') {
      //   const point = c.vertices[0];

      //   if (Math.abs(point[0] - mousePoint.x) < epsilon) {
      //     snaps.push({
      //       type: SnapType.Line,
      //       param: {
      //         point,
      //         direction: [0, 1],
      //       },
      //     });
      //   } else if (Math.abs(point[1] - mousePoint.y) < epsilon) {
      //     snaps.push({
      //       type: SnapType.Line,
      //       param: {
      //         point,
      //         direction: [1, 0],
      //       },
      //     });
      //   }
      // }

      if (c.type === 'Line') {
        const param = c.params as ILineParams;

        const mouseVec = new Vector2(mousePoint.x, mousePoint.y);
        const startVec = new Vector2(...param.point1);
        const directionVec = new Vector2(...param.point2).sub(startVec);

        const verticalDirectionVec = new Vector2(
          directionVec.y,
          -directionVec.x,
        ).normalize();

        const distance = Math.abs(
          startVec.sub(mouseVec).dot(verticalDirectionVec),
        );

        if (distance < epsilon) {
          this.pushSnapIfDifferentAndLessThan2(snaps, {
            ...this.parseICurveToSnapInfo(c),
            isExtend: true,
          });
        }
      }

      if (c.type === 'ArcOfCircle') {
        const param = c.params as ICircleParams;

        const distance = Math.sqrt(
          (mousePoint.x - param.center[0]) ** 2 +
            (mousePoint.y - param.center[1]) ** 2,
        );

        if (Math.abs(distance - param.radius) < epsilon) {
          this.pushSnapIfDifferentAndLessThan2(snaps, {
            ...this.parseICurveToSnapInfo(c),
            isExtend: true,
          });
        }
      }
    });

    return snaps;
  }

  // 根据鼠标位置获取点位置（世界坐标）
  protected getPointByMouse(mouse: MouseInfo) {
    const mousePoint = ctx2.viewer.sceneSetup.mouseOnPlane(
      mouse.x,
      mouse.y,
      new Vector3(...this.frame[1]),
      new Vector3(...this.frame[0]),
    );
    return this.sketchFrame.toLocal(mousePoint);
  }

  parseICurveToSnapInfo(c: ICurve): SnapInfo {
    if (c.type === 'Point') {
      return {
        type: SnapType.Point,
        curve: c,
        param: {
          point: c.vertices[0],
        },
      } as SnapInfo<SnapType.Point>;
    }

    if (c.type === 'Line') {
      const startPoint = c.vertices[0];
      const endPoint = c.vertices.at(-1);
      return {
        type: SnapType.Line,
        curve: c,
        param: {
          point: startPoint,
          direction: [endPoint[0] - startPoint[0], endPoint[1] - startPoint[1]],
        },
      } as SnapInfo<SnapType.Line>;
    }

    if (c.type === 'Circle' || c.type === 'ArcOfCircle') {
      const params = c.params as ICircleParams;
      return {
        type: SnapType.Circle,
        curve: c,
        param: {
          center: params.center,
          radius: params.radius,
        },
      } as SnapInfo<SnapType.Circle>;
    }

    if (c.type === 'Ellipse' || c.type === 'ArcOfEllipse') {
      const params = c.params as IEllipseParams;
      return {
        type: SnapType.Ellipse,
        curve: c,
        param: {
          center: params.center,
          majorDirection: params.majorDir,
          major: params.major,
          minor: params.minor,
        },
      } as SnapInfo<SnapType.Ellipse>;
    }

    return {
      type: SnapType.Spline,
      curve: c,
    } as SnapInfo<SnapType.Spline>;
  }

  isSameConstraint(snap1: SnapInfo, snap2: SnapInfo) {
    if (
      snap1.type !== snap2.type ||
      snap1.type === SnapType.Spline ||
      snap2.type === SnapType.Spline
    ) {
      return false;
    }

    if (snap1.type === SnapType.Line) {
      const param1 = snap1.param as LineSnapParam;
      const param2 = snap2.param as LineSnapParam;

      const direction1 = new Vector2(...param1.direction).normalize();
      const direction2 = new Vector2(...param2.direction).normalize();

      return Math.abs(direction1.cross(direction2)) < EPSILON;
    }

    if (snap1.type === SnapType.Circle) {
      return isEqual(snap1.param, snap2.param);
    }

    if (snap1.type === SnapType.Ellipse) {
      const param1 = snap1.param as EllipseSnapParam;
      const param2 = snap2.param as EllipseSnapParam;

      const dir1 = new Vector2(...param1.majorDirection).normalize();
      const dir2 = new Vector2(...param2.majorDirection).normalize();

      return (
        param1.center === param2.center &&
        param1.major === param2.major &&
        param1.minor === param2.minor &&
        Math.abs(dir1.cross(dir2)) < EPSILON
      );
    }

    return false;
  }

  calcCross(
    snap1: SnapInfo,
    snap2: SnapInfo,
    point: Vector2Tuple,
  ): Vector2Tuple {
    if (snap1 == null || snap2 == null) {
      return null;
    }
    if (snap1.type === SnapType.Line && snap2.type === SnapType.Line) {
      const param1 = snap1.param as LineSnapParam;
      const param2 = snap2.param as LineSnapParam;

      const [x1, y1] = param1.point;
      const k1 = param1.direction[1] / param1.direction[0];

      const [x2, y2] = param2.point;
      const k2 = param2.direction[1] / param2.direction[0];

      if (
        k1 === k2 ||
        (Math.abs(k1) === Math.abs(k2) && Math.abs(k2) === Infinity)
      ) {
        return null;
      }

      let x = 0;

      if (Math.abs(k1) === Infinity) {
        x = x1;
      } else if (Math.abs(k2) === Infinity) {
        x = x2;
      } else {
        x = (y2 - y1 + k1 * x1 - k2 * x2) / (k1 - k2);
      }

      const y =
        Math.abs(k1) === Infinity ? k2 * (x - x2) + y2 : k1 * (x - x1) + y1;

      return [x, y];
    }

    if (
      (snap1.type === SnapType.Line && snap2.type === SnapType.Circle) ||
      (snap1.type === SnapType.Circle && snap2.type === SnapType.Line)
    ) {
      const lineParam = (
        snap1.type === SnapType.Line ? snap1.param : snap2.param
      ) as LineSnapParam;

      const circleParam = (
        snap1.type === SnapType.Line ? snap2.param : snap1.param
      ) as CircleSnapParam;

      const centerVector = new Vector2(...circleParam.center);
      const directionVector = new Vector2(...lineParam.direction);

      // 圆心作直线的垂线得到的垂足
      const verticalPoint = calcNearestPointOnLine(
        circleParam.center,
        lineParam.point,
        lineParam.direction,
      );

      // 垂线长度的平方
      const heightSq = new Vector2()
        .subVectors(verticalPoint, centerVector)
        .lengthSq();

      const walkLength = Math.sqrt(circleParam.radius ** 2 - heightSq);

      const points = [
        new Vector2().addVectors(
          verticalPoint,
          directionVector.clone().setLength(walkLength),
        ),
        new Vector2().addVectors(
          verticalPoint,
          directionVector.clone().setLength(-walkLength),
        ),
      ];

      const nearPoint = new Vector2(...point);

      const result = points.sort((a, b) => {
        const vecA = new Vector2().subVectors(a, nearPoint);
        const vecB = new Vector2().subVectors(b, nearPoint);

        return vecA.lengthSq() - vecB.lengthSq();
      })[0];

      return [result.x, result.y];
    }

    if (snap1.type === SnapType.Circle && snap2.type === SnapType.Circle) {
      const param1 = snap1.param as CircleSnapParam;
      const param2 = snap2.param as CircleSnapParam;

      const radius1 = param1.radius;
      const radius2 = param2.radius;
      const distanceVector = new Vector2(
        param2.center[0] - param1.center[0],
        param2.center[1] - param1.center[1],
      );

      const distance = distanceVector.length();

      // 直接代入了计算的公式结果
      const d1 = (radius1 ** 2 - radius2 ** 2) / 2 / distance + distance / 2;
      const hSquare = radius1 ** 2 - d1 ** 2;

      const h = Math.sqrt(hSquare);

      const c1 = new Vector2(...param1.center);

      const verticalPoint = new Vector2().addVectors(
        c1,
        distanceVector.setLength(Math.sqrt(radius1 ** 2 - hSquare)),
      );

      const points = [
        new Vector2().addVectors(
          verticalPoint,
          new Vector2(distanceVector.y, -distanceVector.x).setLength(h),
        ),
        new Vector2().addVectors(
          verticalPoint,
          new Vector2(distanceVector.y, -distanceVector.x).setLength(-h),
        ),
      ];

      const nearPoint = new Vector2(...point);

      const result = points.sort((a, b) => {
        const vecA = new Vector2().subVectors(a, nearPoint);
        const vecB = new Vector2().subVectors(b, nearPoint);

        return vecA.lengthSq() - vecB.lengthSq();
      })[0];

      return [result.x, result.y];
    }

    return null;
  }

  calcNearestPoint(mousePoint: Vector2Tuple, snapInfo: SnapInfo): Vector2Tuple {
    let resultInfo: { point: Vector2Tuple; distance: number };

    if (snapInfo.type === SnapType.Point) {
      return (snapInfo.param as PointSnapParam).point;
    }
    if (snapInfo.type === SnapType.Circle) {
      const param = snapInfo.param as CircleSnapParam;

      const end = new Vector2(...mousePoint);
      const center = new Vector2(...param.center);

      const result = new Vector2().addVectors(
        center,
        end.sub(center).setLength(param.radius),
      );

      return [result.x, result.y];
    }
    if (snapInfo.type === SnapType.Line) {
      const snapParams = (snapInfo as SnapInfo<SnapType.Line>).param;
      const point = calcNearestPointOnLine(
        mousePoint,
        snapParams.point,
        snapParams.direction,
      );

      return [point.x, point.y];
    }

    if (snapInfo.curve != null) {
      let resultIndex: number;

      snapInfo.curve.vertices.forEach((v, i) => {
        const distance = Math.sqrt(
          (v[0] - mousePoint[0]) ** 2 + (v[1] - mousePoint[1]) ** 2,
        );
        if (resultInfo == null || resultInfo.distance > distance) {
          resultInfo = {
            point: v,
            distance,
          };

          resultIndex = i;
        }
      });

      const points = [
        snapInfo.curve.vertices[resultIndex - 1],
        snapInfo.curve.vertices[resultIndex],
        snapInfo.curve.vertices[resultIndex + 1],
      ].filter((p) => p != null);

      if (points.length === 2) {
        return calcNearestPointOnLine(mousePoint, points[0], [
          points[1][0] - points[0][0],
          points[1][1] - points[0][1],
        ]).toArray();
      }

      if (points.length === 3) {
        const closePoints = [
          calcNearestPointOnLine(mousePoint, points[0], [
            points[1][0] - points[0][0],
            points[1][1] - points[0][1],
          ]),
          calcNearestPointOnLine(mousePoint, points[1], [
            points[2][0] - points[0][0],
            points[2][1] - points[0][1],
          ]),
        ];

        const getDistance = (v: Vector2) => {
          return new Vector2(v.x - mousePoint[0], v.y - mousePoint[1]).length();
        };

        return getDistance(closePoints[0]) > getDistance(closePoints[1])
          ? closePoints[1].toArray()
          : closePoints[0].toArray();
      }
    }

    return null;
  }
}

const calcNearestPointOnLine = (
  point: Vector2Tuple,
  lineStart: Vector2Tuple,
  lineDirection: Vector2Tuple,
) => {
  const pointVec = new Vector2(...point);
  const lineStartVec = new Vector2(...lineStart);

  const verticalDirectionVec = new Vector2(
    lineDirection[1],
    -lineDirection[0],
  ).normalize();

  const result = new Vector2().addVectors(
    pointVec,
    verticalDirectionVec.setLength(
      verticalDirectionVec.dot(lineStartVec.sub(pointVec)),
    ),
  );

  return result;
};

let _snapManager: SnapManager;

export const snapManager = () => {
  if (_snapManager == null) {
    _snapManager = new SnapManager();
  }

  return _snapManager;
};
